SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
3.5 KB · 88 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { KV, Row } from '@/components/ui/key-value';5import { Container, Note } from '@/components/ui/section';6import { api, ApiError } from '@/lib/api';7import { fmtDateTime, fmtInt } from '@/lib/format';8import { routes } from '@/lib/site';910export const revalidate = 3600;11export const metadata: Metadata = { title: 'Snapshot', robots: { index: false } };1213export default async function SnapshotPage({ params }: { params: Promise<{ id: string }> }) {14  const { id } = await params;15  let s;16  try {17    s = await api.snapshot(id);18  } catch (e) {19    if (e instanceof ApiError && e.notFound) notFound();20    throw e;21  }22  return (23    <Container>24      <div className="pb-4 pt-6 md:pt-10">25        <p className="eyebrow">Snapshot · version {s.version_no}</p>26        <h1 className="display mt-2 text-[24px] md:text-[32px]">{s.title ?? 'Normalised page version'}</h1>27        <p className="mt-2 text-sm text-ink-2">28          Fetched {fmtDateTime(s.fetched_at)} ·{' '}29          <Link href={routes.sensor(s.sensor_id)} className="link">30            sensor31          </Link>32          {s.previous_snapshot_id && (33            <>34              {' · '}35              <Link href={routes.snapshotDiff(s.previous_snapshot_id, s.id)} className="link">36                diff against previous version37              </Link>38            </>39          )}40        </p>41      </div>42      <div className="grid gap-8 lg:grid-cols-12">43        <article className="lg:col-span-8">44          <p className="eyebrow mb-2">Semantic blocks as observed</p>45          {s.blocks?.length ? (46            <ol className="space-y-2">47              {s.blocks.map((b) => (48                <li key={b.key} className="border-l-2 border-rule pl-3">49                  <p className="mono text-[10px] uppercase tracking-wider text-ink-3">50                    {b.kind} <span className="normal-case tracking-normal">· {b.path}</span>51                  </p>52                  <p className={`mt-0.5 text-sm ${b.kind === 'heading' ? 'font-semibold text-ink' : 'text-ink-2'}`}>{b.text}</p>53                </li>54              ))}55            </ol>56          ) : (57            <pre className="whitespace-pre-wrap border border-rule bg-surface-2 p-3 text-sm">{s.text}</pre>58          )}59        </article>60        <aside className="lg:col-span-4">61          <KV>62            <Row k="Language">{s.language ?? '—'}</Row>63            <Row k="Text length">64              <span className="tnum">{fmtInt(s.text_length)}</span>65            </Row>66            <Row k="Blocks">67              <span className="tnum">{fmtInt(s.block_count)}</span>68            </Row>69            <Row k="Content hash">70              <span className="mono text-xs break-all">{s.content_hash}</span>71            </Row>72            <Row k="Id">73              <span className="mono text-xs break-all">{s.id}</span>74            </Row>75          </KV>76          {Object.keys(s.extracted ?? {}).length > 0 && (77            <div className="mt-4">78              <p className="eyebrow mb-1">Extracted fields</p>79              <pre className="overflow-x-auto border border-rule bg-surface-2 p-3 text-xs">{JSON.stringify(s.extracted, null, 2)}</pre>80            </div>81          )}82          <Note className="mt-4">This is the normalised representation (scripts, styles, tracking parameters and rotating content removed). The raw object is kept in the content-addressed archive; identical pages share one object.</Note>83        </aside>84      </div>85    </Container>86  );87}88